-----------------Biomes----------------
A 4am crack                  2017-09-15
---------------------------------------

Name: Biomes
Genre: educational
Year: 1986
Credits: developed by John Boeschen and
  Co.; programmers: Jon Newhall, Steve
  A. Baker; artist: John Grandy;
  consultant: Jeffrey Kaufmann
Publisher: D.C. Heath and Company
Platform: Apple ][+ or later (64K)
Media: single-sided 5.25-inch floppy
OS: ProDOS 1.1.1
Previous cracks: none

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  read error on first pass

Locksmith Fast Disk Backup
  unable to copy track $03; copy boots
  ProDOS then hangs with drive motor on

EDD 4 bit copy (no sync, no count)
  no errors, but copy boots ProDOS then
  says "PLACE THE BIOMES DISK IN A
  FLOPPY DRIVE AND RESTART THE PROGRAM"

Passport
  finds "nibble count protection" on
  track 3 but doesn't apply any patches

Copy ][+ nibble editor
  track 3 is almost entirely sync bytes

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK: 03  START: 1800  LENGTH: 3DFF

27F0: FF FF FF FF FF FF FF FF   VIEW
27F8: FF FF FF FF FF FF FF FF
2800: FF FF FF FF FF FF FF FF
2808: FF FF FF FF FF FF FF FF
2810: D5 FF FF FF FF FF FF FF  <-2810
2818: FF FF FF FF FF FF FF FF
2820: FF FF FF FF FF FF FF FF
2828: FF FF FF FF FF FF FF FF
2830: FF FF FF FF FF FF FF FF

                 --^--

Disk Fixer
  track 0 looks like standard ProDOS
  bootloader, including disk catalog;
  no way to read track 3 (no sectors)

Why didn't COPYA work?
  whole-track protection sequence on
  track 3

Why didn't Locksmith FDB work?
  ditto

Why didn't my EDD copy work?
  a runtime check (probably in the
  startup program) that's examining the
  specially formatted track 3

Next steps:

  1. Trace the startup program
  2. Disable the protection check
  3. Declare victory (*)

(*) Go to the gym

                   ~

               Chapter 1
        In Which We Will Not Be
    Going To The Gym Any Time Soon


The disk presents as a standard ProDOS
disk, so let's take advantage of that.

[S6,D1=non-working copy]
[S7,D1=my ProDOS hard drive /A4AMCRACK]

]PR#7
...
]CAT,S6,D1

/PROBIO

 NAME           TYPE  BLOCKS  MODIFIED

*PRODOS          SYS      30  18-SEP-84
 TMS.OBJ         BIN      10  20-MAR-86
 TERRAIN.PIX     BIN      13  16-FEB-86
 BIO.SYSTEM      SYS      27  21-MAR-86
 FONTSET.BLOCK   BIN       3  28-OCT-85
 MISSION.OBJ     BIN      12  21-MAR-86
 BRIEF.OBJ       BIN      11  21-MAR-86
 SIM.OBJ         BIN       7  21-MAR-86
 EARTH.PD        BIN      10  27-JAN-86
 LOGO            BIN      17  11-OCT-85
 CONGRAT         BIN      12   9-DEC-85
 TIME            BIN      14   9-DEC-85
 BASECMDR        BIN      17  10-DEC-85
 EXCUSEME        BIN      11   8-DEC-85
 HIRES.TBL       BIN       1  19-JAN-86
 ROSTER          BIN       6   6-MAR-86
 HIRES.SOURCE    TXT       5  19-JAN-86
 EARTH.PKED      BIN       8   9-FEB-86

BLOCKS FREE:   51     BLOCKS USED:  229

ProDOS loads the first .SYSTEM file at
address $2000 and transfers control to
it.

]BLOAD BIO.SYSTEM,A$2000,TSYS
]CALL -151

*2000L

2000-   4C 17 52    JMP   $5217

*5217L

; munge reset vector
5217-   4E F4 03    LSR   $03F4

; see below (shifts code in memory)
521A-   20 20 52    JSR   $5220

; jump to new entry point
521D-   4C 6D 1D    JMP   $1D6D

; shift entire program into slightly
; lower memory (necessary because you
; don't get to choose where a .SYSTEM
; file is loaded -- it's always $2000)
5220-   A2 32       LDX   #$32
5222-   F0 08       BEQ   $522C
5224-   A0 FF       LDY   #$FF
5226-   20 2E 52    JSR   $522E
5229-   CA          DEX
522A-   D0 F8       BNE   $5224
522C-   A0 16       LDY   #$16
522E-   B9 00 20    LDA   $2000,Y
5231-   99 67 1D    STA   $1D67,Y
5234-   88          DEY
5235-   C0 FF       CPY   #$FF
5237-   D0 F5       BNE   $522E
5239-   EE 30 52    INC   $5230
523C-   EE 33 52    INC   $5233
523F-   60          RTS

The copy routine is self-contained. I
can run it from the monitor without
losing control.

*5220G

*1D6DL

1D6D-   4E 70 1D    LSR   $1D70
1D70-   DC          ???
1D71-   76 1D       ROR   $1D,X

Well this is unpleasant: self-modifying
mode. The instruction at $1D6D modifies
the instruction at $1D70. The 6502 CPU
had neither an instruction cache nor
execution-only memory protection. You
could literally change the very next
instruction before executing it.

Why? Because it makes it that much more
difficult to reverse engineer it.

Which tells me I'm on the right track.

I can reproduce the self-modification
elsewhere in memory to maintain control
while doing the thing someone doesn't
want me to be doing.

; "LSR $1D70 / RTS"
*300:4E 70 1D 60

*300G

*1D70L

1D70-   6E 76 1D    ROR   $1D76
1D73-   6E 78 1D    ROR   $1D78
1D76-   40          RTI
1D77-   C9 3E       CMP   #$3E

Again with the self-modifying code.

; "ROR $1D76 / ROR $1D78 / RTS"
*300:6E 76 1D 6E 78 1D 60

*300G

*1D76L

1D76-   20 C9 1F    JSR   $1FC9

*1FC9L

1FC9-   6E CC 1F    ROR   $1FCC
1FCC-   DC          ???
1FCD-   D5 1F       CMP   $1F,X

And again.

; "ROR $1FCC / RTS"
*300:6E CC 1F 60

*300G

*1FCCL

1FCC-   6E D5 1F    ROR   $1FD5
1FCF-   6E D8 1F    ROR   $1FD8
1FD2-   A0 21       LDY   #$21
1FD4-   98          TYA
1FD5-   B3          ???
1FD6-   DE 1F 32    DEC   $321F,X

And again.

; "ROR $1FD5 / ROR $1FD8 / RTS"
*300:6E D5 1F 6E D8 1F 60

*300G

*1FD2L

1FD2-   A0 21       LDY   #$21
1FD4-   98          TYA
1FD5-   59 DE 1F    EOR   $1FDE,Y
1FD8-   99 DE 1F    STA   $1FDE,Y
1FDB-   88          DEY
1FDC-   10 F6       BPL   $1FD4

Now a loop that progressively decrypts
the code immediately following the loop
(which works, because no caching).

; above loop + "RTS"
*300:A0 21 98 59 DE 1F 99 DE 1F 88 10
 F6 60

*300G

*1FDEL

1FDE-   A9 AA       LDA   #$AA
1FE0-   A2 02       LDX   #$02
1FE2-   F0 08       BEQ   $1FEC
1FE4-   A0 FF       LDY   #$FF
1FE6-   20 EE 1F    JSR   $1FEE
1FE9-   CA          DEX
1FEA-   D0 F8       BNE   $1FE4
1FEC-   A0 4F       LDY   #$4F
1FEE-   59 79 1D    EOR   $1D79,Y
1FF1-   99 79 1D    STA   $1D79,Y
1FF4-   88          DEY
1FF5-   C0 FF       CPY   #$FF
1FF7-   D0 F5       BNE   $1FEE
1FF9-   EE F0 1F    INC   $1FF0
1FFC-   EE F3 1F    INC   $1FF3
1FFF-   60          RTS

Another progressive decryption loop,
decrypting the code immediately after
the JSR (at $1D76) that got us here in
the first place!

It's also the last thing in this
subroutine, so I can execute it in
place and return to the monitor.

*1FDEG

Now we return to $1D79, which -- not
coincidentally -- we just decrypted
from within the subroutine we called
at $1D76.

My brain hurts.

                   ~

               Chapter 2
              Ow My Brain


*1D79L

; loop through ProDOS devices (looking
; for disks)
1D79-   AC 31 BF    LDY   $BF31
1D7C-   AD 30 BF    LDA   $BF30
1D7F-   59 32 BF    EOR   $BF32,Y
1D82-   29 F0       AND   #$F0
1D84-   F0 04       BEQ   $1D8A ---+
1D86-   88          DEY            |
1D87-   10 F3       BPL   $1D7C    |
                                   |
; should never reach here          |
1D89-   00          BRK            |
                                   |
1D8A-   AE 31 BF    LDX   $BF31 <--+
1D8D-   20 B6 1D    JSR   $1DB6

*1DB6L

1DB6-   B9 32 BF    LDA   $BF32,Y
1DB9-   29 0F       AND   #$0F
1DBB-   C9 00       CMP   #$00
1DBD-   60          RTS

Continuing from $1D90...

1D90-   D0 13       BNE   $1DA5

; save X, Y, and zero page $01
1D92-   98          TYA
1D93-   48          PHA
1D94-   8A          TXA
1D95-   48          PHA
1D96-   A5 01       LDA   $01
1D98-   48          PHA
1D99-   20 C4 1D    JSR   $1DC4

*1DC4L

; seek drive using a raw block read
; (ProDOS MLI command $80)
1DC4-   B9 32 BF    LDA   $BF32,Y
1DC7-   29 F0       AND   #$F0
1DC9-   8D BF 1D    STA   $1DBF
1DCC-   20 00 BF    JSR   $BF00
1DCF-  [80 BE 1D]
1DD2-   F0 02       BEQ   $1DD6
1DD4-   38          SEC
1DD5-   60          RTS

; if the block read worked, save even
; more stuff on the stack
1DD6-   A5 00       LDA   $00
1DD8-   48          PHA
1DD9-   AD BF 1D    LDA   $1DBF
1DDC-   29 70       AND   #$70
1DDE-   85 00       STA   $00
1DE0-   AA          TAX

; turn on drive motor manually (!)
1DE1-   BD 89 C0    LDA   $C089,X

; not shown, but this moves the drive
; head one track (very clever -- we
; couldn't do a block read on track 3
; because there is no structure on the
; track, so we let ProDOS seek to the
; neighboring track then move the drive
; head manually)
1DE4-   A0 04       LDY   #$04
1DE6-   A9 06       LDA   #$06
1DE8-   20 F3 1E    JSR   $1EF3

1DEB-   20 8E 1E    JSR   $1E8E

*1E8EL

; find $D5 nibble
1E8E-   BD 8C C0    LDA   $C08C,X
1E91-   10 FB       BPL   $1E8E
1E93-   48          PHA
1E94-   68          PLA
1E95-   C9 D5       CMP   #$D5
1E97-   D0 F5       BNE   $1E8E

; initialize a checksum
1E99-   A0 00       LDY   #$00
1E9B-   84 01       STY   $01

; count number of $F7 nibbles before
; another $D5 nibble
1E9D-   BD 8C C0    LDA   $C08C,X
1EA0-   10 FB       BPL   $1E9D
1EA2-   C9 D5       CMP   #$D5
1EA4-   F0 0D       BEQ   $1EB3
1EA6-   C9 F7       CMP   #$F7
1EA8-   D0 01       BNE   $1EAB
1EAA-   C8          INY

; the sum of the nibbles themselves
; constitutes the checksum
1EAB-   18          CLC
1EAC-   65 01       ADC   $01
1EAE-   85 01       STA   $01
1EB0-   4C 9D 1E    JMP   $1E9D
1EB3-   98          TYA
1EB4-   F0 E3       BEQ   $1E99

; skip $FF nibbles
1EB6-   BD 8C C0    LDA   $C08C,X
1EB9-   10 FB       BPL   $1EB6
1EBB-   48          PHA
1EBC-   68          PLA
1EBD-   C9 FF       CMP   #$FF
1EBF-   F0 F5       BEQ   $1EB6

; if next nibble is $D5, fail
1EC1-   C9 D5       CMP   #$D5
1EC3-   F0 2C       BEQ   $1EF1

; skip several more nibbles
1EC5-   A0 05       LDY   #$05
1EC7-   BD 8C C0    LDA   $C08C,X
1ECA-   10 FB       BPL   $1EC7
1ECC-   48          PHA
1ECD-   68          PLA
1ECE-   88          DEY
1ECF-   D0 F6       BNE   $1EC7

; skip $FF nibbles
1ED1-   BD 8C C0    LDA   $C08C,X
1ED4-   10 FB       BPL   $1ED1
1ED6-   48          PHA
1ED7-   68          PLA
1ED8-   C9 FF       CMP   #$FF
1EDA-   F0 F5       BEQ   $1ED1

; if next nibble is not $D5, fail
1EDC-   C9 D5       CMP   #$D5
1EDE-   D0 11       BNE   $1EF1

; skip $FF nibbles
1EE0-   BD 8C C0    LDA   $C08C,X
1EE3-   10 FB       BPL   $1EE0
1EE5-   C9 FF       CMP   #$FF
1EE7-   D0 08       BNE   $1EF1

; verify checksum, branch on failure
1EE9-   A5 01       LDA   $01
1EEB-   C9 10       CMP   #$10
1EED-   D0 02       BNE   $1EF1

; success path falls through to here --
; clear carry and return
1EEF-   18          CLC
1EF0-   60          RTS

; failure path is here -- set carry and
; return
1EF1-   38          SEC
1EF2-   60          RTS

Continuing from $1DEE...

; save flags
1DEE-   08          PHP

; restore drive head position -- moving
; back to the track ProDOS thought we
; never left
1DEF-   A0 06       LDY   #$06
1DF1-   A9 04       LDA   #$04
1DF3-   20 F3 1E    JSR   $1EF3

; restore flags
1DF6-   28          PLP

; turn off drive motor and exit (with
; at least the carry flag intact)
1DF7-   A6 00       LDX   $00
1DF9-   BD 88 C0    LDA   $C088,X
1DFC-   68          PLA
1DFD-   85 00       STA   $00
1DFF-   60          RTS

Now continuing from $1D9C...

*1D9CL

; restore everything we saved earlier
1D9C-   68          PLA
1D9D-   85 01       STA   $01
1D9F-   68          PLA
1DA0-   AA          TAX
1DA1-   68          PLA
1DA2-   A8          TAY

; carry clear = protection check
; succeeded, so branch to success path
1DA3-   90 0E       BCC   $1DB3

; otherwise try another slot/drive
1DA5-   CA          DEX
1DA6-   30 08       BMI   $1DB0
1DA8-   88          DEY
1DA9-   10 E2       BPL   $1D8D
1DAB-   AC 31 BF    LDY   $BF31
1DAE-   10 DD       BPL   $1D8D

; ultimate failure -- jump to The
; Badlands (not shown, but trust me,
; it's bad)
1DB0-   4C 00 1E    JMP   $1E00

; execution continues here (from $1DA3)
1DB3-   4C 84 1F    JMP   $1F84

*1F84L

1F84-   20 8D 1F    JSR   $1F8D

*1F8DL

; wipe all the copy protection code
; from memory
1F8D-   A2 02       LDX   #$02
1F8F-   F0 08       BEQ   $1F99
1F91-   A0 FF       LDY   #$FF
1F93-   20 9B 1F    JSR   $1F9B
1F96-   CA          DEX
1F97-   D0 F8       BNE   $1F91
1F99-   A0 1C       LDY   #$1C
1F9B-   99 67 1D    STA   $1D67,Y
1F9E-   88          DEY
1F9F-   C0 FF       CPY   #$FF
1FA1-   D0 F8       BNE   $1F9B
1FA3-   EE 9D 1F    INC   $1F9D
1FA6-   60          RTS

Continuing from $1F87...

1F87-   20 A7 1F    JSR   $1FA7

*1FA7L

; yet another progressive decryption
; loop, for the actual program code
1FA7-   A9 AA       LDA   #$AA
1FA9-   A2 2F       LDX   #$2F
1FAB-   F0 08       BEQ   $1FB5
1FAD-   A0 FF       LDY   #$FF
1FAF-   20 B7 1F    JSR   $1FB7
1FB2-   CA          DEX
1FB3-   D0 F8       BNE   $1FAD
1FB5-   A0 7D       LDY   #$7D
1FB7-   59 00 20    EOR   $2000,Y
1FBA-   99 00 20    STA   $2000,Y
1FBD-   88          DEY
1FBE-   C0 FF       CPY   #$FF
1FC0-   D0 F5       BNE   $1FB7
1FC2-   EE B9 1F    INC   $1FB9
1FC5-   EE BC 1F    INC   $1FBC
1FC8-   60          RTS

Continuing from $1F8A...

; start the actual program (now
; decrypted)
1F8A-   4C 00 20    JMP   $2000

Interesting: the protection check is
designed to poll all available floppy
drives. That's why the error message is
phrased the way it is, "PLACE THE DISK
IN *A* FLOPPY DRIVE..." It doesn't care
which one. You could copy the program
files to a hard drive and run it from
there. As long as your original program
disk was in a floppy drive, the loop at
$1D79 would eventually find it and the
protection check would pass.

(Counterpoint: if you have another
ProDOS disk in a floppy drive and it
finds that one first, the code at $1E8E
will hang forever looking for the
proper stream on track 3. That's what
happened with my sector copy.)

I wouldn't call any protection scheme
"user friendly," but this one does
intentionally enable certain use cases.

Now let's burn it to the ground.

                   ~

               Chapter 3
             A Light Touch


On the bright side, the protection
check does not appear to be integrated
with the rest of the program. It zeroes
itself out, decrypts the main program,
and is never heard from again. So I've
got that going for me, which is nice.

On the less-than-bright side, I can't
skip the protection check altogether
because the main program is encrypted
on disk and decrypted by a routine that
runs after the protection check but is
itself decrypted by a separate routine
that runs before the protection check.
It's non-trivial to patch the check
itself because of the progressive
encryption, so where does that leave
us?

One heavy-handed solution would be to
save out the decrypted program code to
a new .SYSTEM file. It even starts at
$2000, which is where all .SYSTEM files
are loaded anyway.

A less invasive approach would be to
leave the protection intact but never
call it. The initial code at $5217
exited via $1D6D, which decrypts and
calls the protection check. My
replacement routine could call $1FC9
directly (to decrypt the protection
code and the success path), then exit
via $1F84 (the success path).

The only caveat is the code at $1FC9 is
self-modifying and assumes the carry
flag is clear on entry. (The first
modification is a ROR, which rotates
the carry into the byte value.)

Thus:

*BLOAD BIO.SYSTEM,A$2000,TSYS

*521E:40 52
*5240:18 20 C9 1F 4C 84 1F

*5217L

5217-   4E F4 03    LSR   $03F4
521A-   20 20 52    JSR   $5220
521D-   4C 40 52    JMP   $5240 --+
...                               |
5240-   18          CLC         <-+
5241-   20 C9 1F    JSR   $1FC9
5244-   4C 84 1F    JMP   $1F84

*BSAVE BIO.SYSTEM,A$2000,L12871,TSYS

]PR#6
...works, and it is glorious...

Quod erat liberandum.

---------------------------------------
A 4am crack                    No. 1424
------------------EOF------------------
